package com.xiashitech.agent.instrumentation.httprequest.inst;

import com.xiashitech.agent.cache.status.CacheStatus;
import com.xiashitech.agent.config.StartConfig;
import com.xiashitech.agent.constants.Instrumentation;
import com.xiashitech.agent.constants.SpecialConfigString;
import com.xiashitech.agent.dto.UserRequest;
import com.xiashitech.agent.instrumentation.bridge.Java8Bridge;
import com.xiashitech.agent.instrumentation.helper.calldepth.XSHttpRequestCallDepth;
import com.xiashitech.agent.instrumentation.utils.BaggageUtil;
import com.xiashitech.agent.utils.DurationPrinter;
import com.xiashitech.agent.utils.JsonUtil;
import com.xiashitech.agent.utils.SessionUtil;
import com.xiashitech.agent.utils.TypeUtils;
import com.xiashitech.interfaces.agent.dto.AgentConfigDTO;
import io.opentelemetry.javaagent.bootstrap.CallDepth;
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer;
import io.opentelemetry.javaagent.shaded.io.opentelemetry.api.baggage.Baggage;
import io.opentelemetry.javaagent.shaded.io.opentelemetry.context.Context;
import io.opentelemetry.javaagent.shaded.io.opentelemetry.context.Scope;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.matcher.ElementMatcher;
import ognl.Ognl;

import javax.servlet.http.HttpServletRequest;
import java.util.Optional;

import static com.xiashitech.agent.instrumentation.httprequest.servlet.XSHttpRequestSingletons.instrumenter;
import static io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge.currentContext;
import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasSuperType;
import static net.bytebuddy.matcher.ElementMatchers.*;

public class XSHttpRequestInstrumentation implements TypeInstrumentation {

    @Override
    public ElementMatcher<TypeDescription> typeMatcher() {
        return hasSuperType(namedOneOf("javax.servlet.http.HttpServlet","javax.servlet.http.HttpFilter"));
    }

    @Override
    public void transform(TypeTransformer typeTransformer) {
        typeTransformer.applyAdviceToMethod(
                isMethod().and(namedOneOf("service","doFilter"))
                        .and(takesArgument(0, named("javax.servlet.http.HttpServletRequest")))
                        .and(takesArgument(1, named("javax.servlet.http.HttpServletResponse")))
                        .and(isProtected()),
                XSHttpRequestInstrumentation.class.getName() + "$FilterAndServletAdvice");
    }

    public static class FilterAndServletAdvice {

        @Advice.OnMethodEnter(suppress = Throwable.class)
        public static void onEnter(
                @Advice.Argument(value = 0, readOnly = false) HttpServletRequest request,
                @Advice.Local("otelCallDepth") CallDepth callDepth,
                @Advice.Local("otelContext") Context context,
                @Advice.Local("otelScope") Scope scope,
                @Advice.Local("user") UserRequest userRequest) {
            DurationPrinter durationPrinter = new DurationPrinter("XSHttpRequestInstrumentation");
            if(CacheStatus.cannotRun()) {
                return;
            }
            callDepth = CallDepth.forClass(XSHttpRequestCallDepth.class);
            if (callDepth.getAndIncrement() > 0) {
                return;
            }
            try {
//                if(!(request instanceof XSHttpServletRequestWrapper)) {
//                    String loginPathKey = AgentConfigDTO.getConfig().getLoginPath().get(StartConfig.getParamValueFromKey(StartConfig.systemId));
//                    if(loginPathKey != null && request.getRequestURI().contains(loginPathKey) && request.getContentType().toLowerCase().contains("json")) {
//                        request = new XSHttpServletRequestWrapper(request);
//                    }
//                }
                String host = request.getHeader("host");
                // 对于静态资源，如 css、image、js 等不做处理
                String acceptType = request.getHeader("Accept");
                if(TypeUtils.isSkipAcceptType(acceptType, request.getRequestURI())) {
                    return;
                }
                // 已经存在 userId 就不继续
                String uid = Java8Bridge.currentForBaggage().getEntryValue("xs.userId");
                if(uid != null && !"".equals(uid) && !"pcsa".equals(uid) && !"anonymousUser".equalsIgnoreCase(uid))
                    return;

                Optional<UserRequest> userRequestOptional = Java8Bridge.executeRuleForUserInfoMatchRule(request);
                if(userRequestOptional.isPresent()) {
                    userRequest = userRequestOptional.get();
                } else {
                    userRequest = UserRequest.builder().build();
                }
                userRequest.setRequest(request);
                Context parentContext = currentContext();
                if(Java8Bridge.currentForBaggage().getEntryValue("xsFlowStart") == null) {
                    Baggage xsFlowStartBaggage = Java8Bridge.currentForBaggage().toBuilder()
                            .put("xsFlowStart", "true")
                            .put("xsAuthSystemId", StartConfig.getParamValueFromKey(StartConfig.systemId))
                            .build();
                    parentContext = parentContext.with(xsFlowStartBaggage);
                }
                if (instrumenter().shouldStart(parentContext,userRequest)) {
                    parentContext = BaggageUtil.syncBaggage().build().storeInContext(parentContext);
                    context = instrumenter().start(parentContext, userRequest);

                    // 尝试从 SecurityContextHolder 直接 getAuthentication 方式来获取下
                    Object securityContext = null;
                    try {
                        securityContext = Class.forName("org.springframework.security.core.context.SecurityContextHolder").getMethod("getContext").invoke(null);
                    } catch ( Exception ea) {
                        if(!"java.lang.ClassNotFoundException".equalsIgnoreCase(ea.getClass().getName()))
                            StartConfig.throwableStackPrinter(ea);
                    }
                    if(securityContext != null) {
                        Object authentication = securityContext.getClass().getMethod("getAuthentication").invoke(securityContext);
                        // String userstr = AuthenticationUtil.getFromAuthentication(authentication);
                         String userstr = "";
                         // 使用 userIdPath 为特定字符串来决定是否把所有数据取上来
                         if(authentication != null && Class.forName("org.springframework.security.core.Authentication").isAssignableFrom(authentication.getClass())) {
                             Object principal = authentication.getClass().getMethod("getPrincipal").invoke(authentication);
                             if(principal != null) {
                                 String userIdPath = AgentConfigDTO.getConfig().getUseridpath().get(StartConfig.getParamValueFromKey(StartConfig.systemId));
                                 if (SpecialConfigString.ALL_AUTHENTICATION.getConfiString().equalsIgnoreCase(userIdPath)) { // 这个配置，因为会把整个 authentication 放到 http 头信息中，可能会破坏 http 请求
                                     userstr = principal.toString() + "##" + JsonUtil.convertObjectToString(authentication);
                                 } else {
                                     if (principal.getClass() == String.class)
                                         userstr = principal.toString();
                                     else // SOC 系统：userIdPath 配置为：username，如果取 id，就配 id
                                         try {
                                             userstr = (String) Ognl.getValue(userIdPath, principal);
                                         } catch (Exception e) {
                                             // 用户ID 有可能是匹配了 rule.usermatch 包里的规则获取的，这里打堆栈会导致此种情况控制台一直输出大片堆栈信息
                                             // 故简单注释掉
                                             // StartConfig.throwableStackPrinter(e);
                                         }
                                 }
                             }
                         }
                         if(userstr != null && !userstr.isEmpty() ) // && !"anonymousUser".equalsIgnoreCase(userstr)
                             userRequest.setUserid(userstr);
                    }

                    String accessToken = AgentConfigDTO.getConfig().getAccessToken().get(StartConfig.getParamValueFromKey(StartConfig.systemId));
                    String token = "";
                    if(accessToken != null && !"".equals(accessToken)) {
                        token = request.getHeader(accessToken);
                        if(token != null && !"".equals(token) && token.startsWith(Instrumentation.AUTHORIZATION_BEARER)) {
                            int index = token.indexOf(Instrumentation.AUTHORIZATION_BEARER + " ");
                            if(index >= 0) {
                                token = token.substring(index + 7).trim();
                            }
                        } else {
                            token = SessionUtil.getCookieValueByKey(request,accessToken);
                        }
                    }
                    context = BaggageUtil.syncBaggage()
                            .put("xs.userId",userRequest.getUserid() == null ? "" : userRequest.getUserid())
                            .put("xs.loginName",userRequest.getLoginName() == null ? "" : userRequest.getLoginName())
                            .put("xs.sessionId",userRequest.getSessionId() == null ? "" : userRequest.getSessionId())
                            .put("xs.requestDate",userRequest.getRequestDate() == null ? "" : userRequest.getRequestDate().toString())
                            .put("xs.sessionUpdate",userRequest.getSessionUpdate() == null ? "false" : userRequest.getSessionUpdate().toString())
                            .put("xs.logintoken",token == null || "".equals(token) ? "" : token)
                            .build().storeInContext(context);
                    scope = context.makeCurrent();
                }
            } catch (Throwable e) {
                StartConfig.throwableStackPrinter(e);
            }
            durationPrinter.printElapseDuration("FilterAndServletAdvice b");
        }

        @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
        public static void onExit(
                @Advice.Argument(0) HttpServletRequest request,
                @Advice.Local("otelCallDepth") CallDepth callDepth,
                @Advice.Thrown Throwable throwable,
                @Advice.Local("otelContext") Context context,
                @Advice.Local("otelScope") Scope scope,
                @Advice.Local("user") UserRequest userRequest) {
            DurationPrinter durationPrinter = new DurationPrinter("XSHttpRequestInstrumentation");
            if(CacheStatus.cannotRun()) {
                return;
            }
            if (callDepth.decrementAndGet() > 0) {
                return;
            }
            try {
                if (scope == null) {
                    return;
                }
                scope.close();
                // 已经存在 userId 就不继续 (会导致不Trace不连续)
                //String uid = Java8Bridge.currentForBaggage().getEntryValue("xs.userId");
                //if(uid != null && !"".equals(uid) && !"pcsa".equals(uid) && !"anonymousUser".equalsIgnoreCase(uid)){
                    //return;
                //}

                if(userRequest.getUserid() == null || "".equals(userRequest.getUserid())) {
                    Optional<UserRequest> userRequestOptional = Java8Bridge.executeRuleForUserInfoMatchRule(request);
                    if(userRequestOptional.isPresent() && userRequestOptional.get().getUserid() != null && !"".equals(userRequestOptional.get().getUserid())) {
                        userRequest = userRequestOptional.get();
                        userRequest.setSessionUpdate(true);
                    }
                }
                instrumenter().end(context,userRequest,null,throwable);
            } catch (Throwable e) {
                StartConfig.throwableStackPrinter(e);
            }
            durationPrinter.printElapseDuration("FilterAndServletAdvice e");
        }
    }
}
